﻿Option Strict On

Imports System.Text
Imports System.Net
Imports System.Net.Sockets
Imports System.Globalization
Imports System.ComponentModel

Namespace Proxy
    ''' <summary>
    ''' Socks5 connection proxy class.  This class implements the Socks5 standard proxy protocol.
    ''' </summary>
    ''' <remarks>
    ''' This implementation supports TCP proxy connections with a Socks v5 server.
    ''' </remarks>
    Public Class Socks5ProxyClient
        Implements IProxyClient

        Private _proxyHost As String
        Private _proxyPort As Integer
        Private _proxyUserName As String
        Private _proxyPassword As String
        Private _proxyAuthMethod As SocksAuthentication
        Private _tcpClient As TcpClient

        Private Const PROXY_NAME As String = "SOCKS5"
        Private Const SOCKS5_DEFAULT_PORT As Integer = 1080

        Private Const SOCKS5_VERSION_NUMBER As Byte = 5
        Private Const SOCKS5_RESERVED As Byte = &H0
        Private Const SOCKS5_AUTH_NUMBER_OF_AUTH_METHODS_SUPPORTED As Byte = 2
        Private Const SOCKS5_AUTH_METHOD_NO_AUTHENTICATION_REQUIRED As Byte = &H0
        Private Const SOCKS5_AUTH_METHOD_GSSAPI As Byte = &H1
        Private Const SOCKS5_AUTH_METHOD_USERNAME_PASSWORD As Byte = &H2
        Private Const SOCKS5_AUTH_METHOD_IANA_ASSIGNED_RANGE_BEGIN As Byte = &H3
        Private Const SOCKS5_AUTH_METHOD_IANA_ASSIGNED_RANGE_END As Byte = &H7F
        Private Const SOCKS5_AUTH_METHOD_RESERVED_RANGE_BEGIN As Byte = &H80
        Private Const SOCKS5_AUTH_METHOD_RESERVED_RANGE_END As Byte = &HFE
        Private Const SOCKS5_AUTH_METHOD_REPLY_NO_ACCEPTABLE_METHODS As Byte = &HFF
        Private Const SOCKS5_CMD_CONNECT As Byte = &H1
        Private Const SOCKS5_CMD_BIND As Byte = &H2
        Private Const SOCKS5_CMD_UDP_ASSOCIATE As Byte = &H3
        Private Const SOCKS5_CMD_REPLY_SUCCEEDED As Byte = &H0
        Private Const SOCKS5_CMD_REPLY_GENERAL_SOCKS_SERVER_FAILURE As Byte = &H1
        Private Const SOCKS5_CMD_REPLY_CONNECTION_NOT_ALLOWED_BY_RULESET As Byte = &H2
        Private Const SOCKS5_CMD_REPLY_NETWORK_UNREACHABLE As Byte = &H3
        Private Const SOCKS5_CMD_REPLY_HOST_UNREACHABLE As Byte = &H4
        Private Const SOCKS5_CMD_REPLY_CONNECTION_REFUSED As Byte = &H5
        Private Const SOCKS5_CMD_REPLY_TTL_EXPIRED As Byte = &H6
        Private Const SOCKS5_CMD_REPLY_COMMAND_NOT_SUPPORTED As Byte = &H7
        Private Const SOCKS5_CMD_REPLY_ADDRESS_TYPE_NOT_SUPPORTED As Byte = &H8
        Private Const SOCKS5_ADDRTYPE_IPV4 As Byte = &H1
        Private Const SOCKS5_ADDRTYPE_DOMAIN_NAME As Byte = &H3
        Private Const SOCKS5_ADDRTYPE_IPV6 As Byte = &H4

        ''' <summary>
        ''' Authentication itemType.
        ''' </summary>
        Private Enum SocksAuthentication
            ''' <summary>
            ''' No authentication used.
            ''' </summary>
            None
            ''' <summary>
            ''' Username and password authentication.
            ''' </summary>
            UsernamePassword
        End Enum

        ''' <summary>
        ''' Create a Socks5 proxy client object. 
        ''' </summary>
        Public Sub New()
        End Sub

        ''' <summary>
        ''' Creates a Socks5 proxy client object using the supplied TcpClient object connection.
        ''' </summary>
        ''' <param name="tcpClient">A TcpClient connection object.</param>
        Public Sub New(ByVal tcpClient As TcpClient)
            If tcpClient Is Nothing Then
                Throw New ArgumentNullException("tcpClient")
            End If

            _tcpClient = tcpClient
        End Sub

        ''' <summary>
        ''' Create a Socks5 proxy client object.  The default proxy port 1080 is used.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        Public Sub New(ByVal proxyHost As String)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            _proxyHost = proxyHost
            _proxyPort = SOCKS5_DEFAULT_PORT
        End Sub

        ''' <summary>
        ''' Create a Socks5 proxy client object.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyPort">Port used to connect to proxy server.</param>
        Public Sub New(ByVal proxyHost As String, ByVal proxyPort As Integer)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If proxyPort <= 0 OrElse proxyPort > 65535 Then
                Throw New ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535")
            End If

            _proxyHost = proxyHost
            _proxyPort = proxyPort
        End Sub

        ''' <summary>
        ''' Create a Socks5 proxy client object.  The default proxy port 1080 is used.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyUserName">Proxy authentication user name.</param>
        ''' <param name="proxyPassword">Proxy authentication password.</param>
        Public Sub New(ByVal proxyHost As String, ByVal proxyUserName As String, ByVal proxyPassword As String)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If proxyUserName Is Nothing Then
                Throw New ArgumentNullException("proxyUserName")
            End If

            If proxyPassword Is Nothing Then
                Throw New ArgumentNullException("proxyPassword")
            End If

            _proxyHost = proxyHost
            _proxyPort = SOCKS5_DEFAULT_PORT
            _proxyUserName = proxyUserName
            _proxyPassword = proxyPassword
        End Sub

        ''' <summary>
        ''' Create a Socks5 proxy client object.  
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyPort">Port used to connect to proxy server.</param>
        ''' <param name="proxyUserName">Proxy authentication user name.</param>
        ''' <param name="proxyPassword">Proxy authentication password.</param>
        Public Sub New(ByVal proxyHost As String, ByVal proxyPort As Integer, ByVal proxyUserName As String, ByVal proxyPassword As String)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If proxyPort <= 0 OrElse proxyPort > 65535 Then
                Throw New ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535")
            End If

            If proxyUserName Is Nothing Then
                Throw New ArgumentNullException("proxyUserName")
            End If

            If proxyPassword Is Nothing Then
                Throw New ArgumentNullException("proxyPassword")
            End If

            _proxyHost = proxyHost
            _proxyPort = proxyPort
            _proxyUserName = proxyUserName
            _proxyPassword = proxyPassword
        End Sub

        ''' <summary>
        ''' Gets or sets host name or IP address of the proxy server.
        ''' </summary>
        Public Property ProxyHost() As String Implements SFDL.Proxy.Proxy.IProxyClient.ProxyHost
            Get
                Return _proxyHost
            End Get
            Set(ByVal value As String)
                _proxyHost = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets port used to connect to proxy server.
        ''' </summary>
        Public Property ProxyPort() As Integer Implements SFDL.Proxy.Proxy.IProxyClient.ProxyPort
            Get
                Return _proxyPort
            End Get
            Set(ByVal value As Integer)
                _proxyPort = value
            End Set
        End Property

        ''' <summary>
        ''' Gets String representing the name of the proxy. 
        ''' </summary>
        ''' <remarks>This property will always return the value 'SOCKS5'</remarks>
        Public ReadOnly Property ProxyName() As String Implements SFDL.Proxy.Proxy.IProxyClient.ProxyName
            Get
                Return PROXY_NAME
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets proxy authentication user name.
        ''' </summary>
        Public Property ProxyUserName() As String
            Get
                Return _proxyUserName
            End Get
            Set(ByVal value As String)
                _proxyUserName = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets proxy authentication password.
        ''' </summary>
        Public Property ProxyPassword() As String
            Get
                Return _proxyPassword
            End Get
            Set(ByVal value As String)
                _proxyPassword = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the TcpClient object. 
        ''' This property can be set prior to executing CreateConnection to use an existing TcpClient connection.
        ''' </summary>
        Public Property TcpClient() As TcpClient Implements SFDL.Proxy.Proxy.IProxyClient.TcpClient
            Get
                Return _tcpClient
            End Get
            Set(ByVal value As TcpClient)
                _tcpClient = value
            End Set
        End Property

        ''' <summary>
        ''' Creates a remote TCP connection through a proxy server to the destination host on the destination port.
        ''' </summary>
        ''' <param name="destinationHost">Destination host name or IP address of the destination server.</param>
        ''' <param name="destinationPort">Port number to connect to on the destination host.</param>
        ''' <return>
        ''' return an open TcpClient object that can be used normally to communicate
        ''' with the destination server
        ''' </return>
        ''' <remarks>
        ''' This method creates a connection to the proxy server and instructs the proxy server
        ''' to make a pass through connection to the specified destination host on the specified
        ''' port.  
        ''' </remarks>
        Public Function CreateConnection(ByVal destinationHost As String, ByVal destinationPort As Integer) As TcpClient Implements SFDL.Proxy.Proxy.IProxyClient.CreateConnection
            If String.IsNullOrEmpty(destinationHost) Then
                Throw New ArgumentNullException("destinationHost")
            End If

            If destinationPort <= 0 OrElse destinationPort > 65535 Then
                Throw New ArgumentOutOfRangeException("destinationPort", "port must be greater than zero and less than 65535")
            End If

            Try
                ' if we have no connection, create one
                If _tcpClient Is Nothing Then
                    If String.IsNullOrEmpty(_proxyHost) Then
                        Throw New ProxyException("ProxyHost property must contain a value.")
                    End If

                    If _proxyPort <= 0 OrElse _proxyPort > 65535 Then
                        Throw New ProxyException("ProxyPort value must be greater than zero and less than 65535")
                    End If

                    '  create new tcp client object to the proxy server
                    _tcpClient = New TcpClient()

                    ' attempt to open the connection
                    _tcpClient.Connect(_proxyHost, _proxyPort)
                End If

                '  determine which authentication method the client would like to use
                DetermineClientAuthMethod()

                ' negotiate which authentication methods are supported / accepted by the server
                NegotiateServerAuthMethod()

                ' send a connect command to the proxy server for destination host and port
                SendCommand(SOCKS5_CMD_CONNECT, destinationHost, destinationPort)

                ' return the open proxied tcp client object to the caller for normal use
                Return _tcpClient
            Catch ex As Exception
                Throw New ProxyException(String.Format(CultureInfo.InvariantCulture, "Connection to proxy host {0} on port {1} failed.", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient)), ex)
            End Try
        End Function


        Private Sub DetermineClientAuthMethod()
            '  set the authentication itemType used based on values inputed by the user
            If _proxyUserName IsNot Nothing AndAlso _proxyPassword IsNot Nothing Then
                _proxyAuthMethod = SocksAuthentication.UsernamePassword
            Else
                _proxyAuthMethod = SocksAuthentication.None
            End If
        End Sub

        Private Sub NegotiateServerAuthMethod()
            '  get a reference to the network stream
            Dim stream As NetworkStream = _tcpClient.GetStream()

            ' SERVER AUTHENTICATION REQUEST
            ' The client connects to the server, and sends a version
            ' identifier/method selection message:
            '
            '      +----+----------+----------+
            '      |VER | NMETHODS | METHODS  |
            '      +----+----------+----------+
            '      | 1  |    1     | 1 to 255 |
            '      +----+----------+----------+

            Dim authRequest As Byte() = New Byte(3) {}
            authRequest(0) = SOCKS5_VERSION_NUMBER
            authRequest(1) = SOCKS5_AUTH_NUMBER_OF_AUTH_METHODS_SUPPORTED
            authRequest(2) = SOCKS5_AUTH_METHOD_NO_AUTHENTICATION_REQUIRED
            authRequest(3) = SOCKS5_AUTH_METHOD_USERNAME_PASSWORD

            '  send the request to the server specifying authentication types supported by the client.
            stream.Write(authRequest, 0, authRequest.Length)

            '  SERVER AUTHENTICATION RESPONSE
            '  The server selects from one of the methods given in METHODS, and
            '  sends a METHOD selection message:
            '
            '     +----+--------+
            '     |VER | METHOD |
            '     +----+--------+
            '     | 1  |   1    |
            '     +----+--------+
            '
            '  If the selected METHOD is X'FF', none of the methods listed by the
            '  client are acceptable, and the client MUST close the connection.
            '
            '  The values currently defined for METHOD are:
            '   * X'00' NO AUTHENTICATION REQUIRED
            '   * X'01' GSSAPI
            '   * X'02' USERNAME/PASSWORD
            '   * X'03' to X'7F' IANA ASSIGNED
            '   * X'80' to X'FE' RESERVED FOR PRIVATE METHODS
            '   * X'FF' NO ACCEPTABLE METHODS

            '  receive the server response 
            Dim response As Byte() = New Byte(1) {}
            stream.Read(response, 0, response.Length)

            '  the first byte contains the socks version number (e.g. 5)
            '  the second byte contains the auth method acceptable to the proxy server
            Dim acceptedAuthMethod As Byte = response(1)

            ' if the server does not accept any of our supported authenication methods then throw an error
            If acceptedAuthMethod = SOCKS5_AUTH_METHOD_REPLY_NO_ACCEPTABLE_METHODS Then
                _tcpClient.Close()
                Throw New ProxyException("The proxy destination does not accept the supported proxy client authentication methods.")
            End If

            ' if the server accepts a username and password authentication and none is provided by the user then throw an error
            If acceptedAuthMethod = SOCKS5_AUTH_METHOD_USERNAME_PASSWORD AndAlso _proxyAuthMethod = SocksAuthentication.None Then
                _tcpClient.Close()
                Throw New ProxyException("The proxy destination requires a username and password for authentication.")
            End If

            If acceptedAuthMethod = SOCKS5_AUTH_METHOD_USERNAME_PASSWORD Then

                ' USERNAME / PASSWORD SERVER REQUEST
                ' Once the SOCKS V5 server has started, and the client has selected the
                ' Username/Password Authentication protocol, the Username/Password
                ' subnegotiation begins.  This begins with the client producing a
                ' Username/Password request:
                '
                '       +----+------+----------+------+----------+
                '       |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
                '       +----+------+----------+------+----------+
                '       | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
                '       +----+------+----------+------+----------+

                Dim credentials As Byte() = New Byte(_proxyUserName.Length + _proxyPassword.Length + 2) {}
                credentials(0) = SOCKS5_VERSION_NUMBER
                credentials(1) = CByte(_proxyUserName.Length)
                Array.Copy(ASCIIEncoding.ASCII.GetBytes(_proxyUserName), 0, credentials, 2, _proxyUserName.Length)
                credentials(_proxyUserName.Length + 2) = CByte(_proxyPassword.Length)

                ' USERNAME / PASSWORD SERVER RESPONSE
                ' The server verifies the supplied UNAME and PASSWD, and sends the
                ' following response:
                '
                '   +----+--------+
                '   |VER | STATUS |
                '   +----+--------+
                '   | 1  |   1    |
                '   +----+--------+
                '
                ' A STATUS field of X'00' indicates success. If the server return a
                ' `failure' (STATUS value other than X'00') status, it MUST close the
                ' connection.
                Array.Copy(ASCIIEncoding.ASCII.GetBytes(_proxyPassword), 0, credentials, _proxyUserName.Length + 3, _proxyPassword.Length)
            End If
        End Sub

        Private Function GetDestAddressType(ByVal host As String) As Byte
            Dim ipAddr As IPAddress = Nothing

            Dim result As Boolean = IPAddress.TryParse(host, ipAddr)

            If Not result Then
                Return SOCKS5_ADDRTYPE_DOMAIN_NAME
            End If

            Select Case ipAddr.AddressFamily
                Case AddressFamily.InterNetwork
                    Return SOCKS5_ADDRTYPE_IPV4
                Case AddressFamily.InterNetworkV6
                    Return SOCKS5_ADDRTYPE_IPV6
                Case Else
                    Throw New ProxyException(String.Format(CultureInfo.InvariantCulture, "The host addess {0} of type '{1}' is not a supported address type.  The supported types are InterNetwork and InterNetworkV6.", host, [Enum].GetName(GetType(AddressFamily), ipAddr.AddressFamily)))
            End Select

        End Function

        Private Function GetDestAddressBytes(ByVal addressType As Byte, ByVal host As String) As Byte()
            Select Case addressType
                Case SOCKS5_ADDRTYPE_IPV4, SOCKS5_ADDRTYPE_IPV6
                    Return IPAddress.Parse(host).GetAddressBytes()
                Case SOCKS5_ADDRTYPE_DOMAIN_NAME
                    '  create a byte array to hold the host name bytes plus one byte to store the length
                    Dim bytes As Byte() = New Byte(host.Length) {}
                    '  if the address field contains a fully-qualified domain name.  The first
                    '  octet of the address field contains the number of octets of name that
                    '  follow, there is no terminating NUL octet.
                    bytes(0) = Convert.ToByte(host.Length)
                    Encoding.ASCII.GetBytes(host).CopyTo(bytes, 1)
                    Return bytes
                Case Else
                    Return Nothing
            End Select
        End Function

        Private Function GetDestPortBytes(ByVal value As Integer) As Byte()
            Dim array As Byte() = New Byte(1) {}
            array(0) = Convert.ToByte(value \ 256)
            array(1) = Convert.ToByte(value Mod 256)
            Return array
        End Function

        Private Sub SendCommand(ByVal command As Byte, ByVal destinationHost As String, ByVal destinationPort As Integer)
            Dim stream As NetworkStream = _tcpClient.GetStream()

            Dim addressType As Byte = GetDestAddressType(destinationHost)
            Dim destAddr As Byte() = GetDestAddressBytes(addressType, destinationHost)
            Dim destPort As Byte() = GetDestPortBytes(destinationPort)

            '  The connection request is made up of 6 bytes plus the
            '  length of the variable address byte array
            '
            '  +----+-----+-------+------+----------+----------+
            '  |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
            '  +----+-----+-------+------+----------+----------+
            '  | 1  |  1  | X'00' |  1   | Variable |    2     |
            '  +----+-----+-------+------+----------+----------+
            '
            ' * VER protocol version: X'05'
            ' * CMD
            '   * CONNECT X'01'
            '   * BIND X'02'
            '   * UDP ASSOCIATE X'03'
            ' * RSV RESERVED
            ' * ATYP address itemType of following address
            '   * IP V4 address: X'01'
            '   * DOMAINNAME: X'03'
            '   * IP V6 address: X'04'
            ' * DST.ADDR desired destination address
            ' * DST.PORT desired destination port in network octet order            

            Dim request As Byte() = New Byte(4 + destAddr.Length + 1) {}
            request(0) = SOCKS5_VERSION_NUMBER
            request(1) = command
            request(2) = SOCKS5_RESERVED
            request(3) = addressType
            destAddr.CopyTo(request, 4)
            destPort.CopyTo(request, 4 + destAddr.Length)

            ' send connect request.
            stream.Write(request, 0, request.Length)

            '  PROXY SERVER RESPONSE
            '  +----+-----+-------+------+----------+----------+
            '  |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
            '  +----+-----+-------+------+----------+----------+
            '  | 1  |  1  | X'00' |  1   | Variable |    2     |
            '  +----+-----+-------+------+----------+----------+
            '
            ' * VER protocol version: X'05'
            ' * REP Reply field:
            '   * X'00' succeeded
            '   * X'01' general SOCKS server failure
            '   * X'02' connection not allowed by ruleset
            '   * X'03' Network unreachable
            '   * X'04' Host unreachable
            '   * X'05' Connection refused
            '   * X'06' TTL expired
            '   * X'07' Command not supported
            '   * X'08' Address itemType not supported
            '   * X'09' to X'FF' unassigned
            '* RSV RESERVED
            '* ATYP address itemType of following address

            Dim response As Byte() = New Byte(254) {}

            ' read proxy server response
            stream.Read(response, 0, response.Length)

            Dim replyCode As Byte = response(1)

            '  evaluate the reply code for an error condition
            If replyCode <> SOCKS5_CMD_REPLY_SUCCEEDED Then
                HandleProxyCommandError(response, destinationHost, destinationPort)
            End If
        End Sub

        Private Sub HandleProxyCommandError(ByVal response As Byte(), ByVal destinationHost As String, ByVal destinationPort As Integer)
            Dim proxyErrorText As String
            Dim replyCode As Byte = response(1)
            Dim addrType As Byte = response(3)
            Dim addr As String = ""
            Dim port As Int16 = 0

            Select Case addrType
                Case SOCKS5_ADDRTYPE_DOMAIN_NAME
                    Dim addrLen As Integer = Convert.ToInt32(response(4))
                    Dim addrBytes As Byte() = New Byte(addrLen - 1) {}
                    For i As Integer = 0 To addrLen - 1
                        addrBytes(i) = response(i + 5)
                    Next
                    addr = System.Text.ASCIIEncoding.ASCII.GetString(addrBytes)
                    Dim portBytesDomain As Byte() = New Byte(1) {}
                    portBytesDomain(0) = response(6 + addrLen)
                    portBytesDomain(1) = response(5 + addrLen)
                    port = BitConverter.ToInt16(portBytesDomain, 0)
                    Exit Select

                Case SOCKS5_ADDRTYPE_IPV4
                    Dim ipv4Bytes As Byte() = New Byte(3) {}
                    For i As Integer = 0 To 3
                        ipv4Bytes(i) = response(i + 4)
                    Next
                    Dim ipv4 As New IPAddress(ipv4Bytes)
                    addr = ipv4.ToString()
                    Dim portBytesIpv4 As Byte() = New Byte(1) {}
                    portBytesIpv4(0) = response(9)
                    portBytesIpv4(1) = response(8)
                    port = BitConverter.ToInt16(portBytesIpv4, 0)
                    Exit Select

                Case SOCKS5_ADDRTYPE_IPV6
                    Dim ipv6Bytes As Byte() = New Byte(15) {}
                    For i As Integer = 0 To 15
                        ipv6Bytes(i) = response(i + 4)
                    Next
                    Dim ipv6 As New IPAddress(ipv6Bytes)
                    addr = ipv6.ToString()
                    Dim portBytesIpv6 As Byte() = New Byte(1) {}
                    portBytesIpv6(0) = response(21)
                    portBytesIpv6(1) = response(20)
                    port = BitConverter.ToInt16(portBytesIpv6, 0)
                    Exit Select
            End Select


            Select Case replyCode
                Case SOCKS5_CMD_REPLY_GENERAL_SOCKS_SERVER_FAILURE
                    proxyErrorText = "a general socks destination failure occurred"
                    Exit Select
                Case SOCKS5_CMD_REPLY_CONNECTION_NOT_ALLOWED_BY_RULESET
                    proxyErrorText = "the connection is not allowed by proxy destination rule set"
                    Exit Select
                Case SOCKS5_CMD_REPLY_NETWORK_UNREACHABLE
                    proxyErrorText = "the network was unreachable"
                    Exit Select
                Case SOCKS5_CMD_REPLY_HOST_UNREACHABLE
                    proxyErrorText = "the host was unreachable"
                    Exit Select
                Case SOCKS5_CMD_REPLY_CONNECTION_REFUSED
                    proxyErrorText = "the connection was refused by the remote network"
                    Exit Select
                Case SOCKS5_CMD_REPLY_TTL_EXPIRED
                    proxyErrorText = "the time to live (TTL) has expired"
                    Exit Select
                Case SOCKS5_CMD_REPLY_COMMAND_NOT_SUPPORTED
                    proxyErrorText = "the command issued by the proxy client is not supported by the proxy destination"
                    Exit Select
                Case SOCKS5_CMD_REPLY_ADDRESS_TYPE_NOT_SUPPORTED
                    proxyErrorText = "the address type specified is not supported"
                    Exit Select
                Case Else
                    proxyErrorText = String.Format(CultureInfo.InvariantCulture, "that an unknown reply with the code value '{0}' was received by the destination", replyCode.ToString(CultureInfo.InvariantCulture))
                    Exit Select
            End Select
            Dim exceptionMsg As String = String.Format(CultureInfo.InvariantCulture, "The {0} concerning destination host {1} port number {2}.  The destination reported the host as {3} port {4}.", proxyErrorText, destinationHost, destinationPort, addr, _
             port.ToString(CultureInfo.InvariantCulture))

            Throw New ProxyException(exceptionMsg)

        End Sub


#Region "Async Methods"

        Private _asyncWorker As BackgroundWorker
        Private _asyncException As Exception
        Private _asyncCancelled As Boolean

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is running.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is running; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsBusy() As Boolean
            Get
                Return If(_asyncWorker Is Nothing, False, _asyncWorker.IsBusy)
            End Get
        End Property

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is cancelled.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is cancelled; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsAsyncCancelled() As Boolean
            Get
                Return _asyncCancelled
            End Get
        End Property

        ''' <summary>
        ''' Cancels any asychronous operation that is currently active.
        ''' </summary>
        Public Sub CancelAsync()
            If _asyncWorker IsNot Nothing AndAlso Not _asyncWorker.CancellationPending AndAlso _asyncWorker.IsBusy Then
                _asyncCancelled = True
                _asyncWorker.CancelAsync()
            End If
        End Sub

        Private Sub CreateAsyncWorker()
            If _asyncWorker IsNot Nothing Then
                _asyncWorker.Dispose()
            End If
            _asyncException = Nothing
            _asyncWorker = Nothing
            _asyncCancelled = False
            _asyncWorker = New BackgroundWorker()
        End Sub

        ''' <summary>
        ''' Event handler for CreateConnectionAsync method completed.
        ''' </summary>
        Public Event CreateConnectionAsyncCompleted As EventHandler(Of CreateConnectionAsyncCompletedEventArgs) Implements SFDL.Proxy.Proxy.IProxyClient.CreateConnectionAsyncCompleted


        ''' <summary>
        ''' Asynchronously creates a remote TCP connection through a proxy server to the destination host on the destination port.
        ''' </summary>
        ''' <param name="destinationHost">Destination host name or IP address.</param>
        ''' <param name="destinationPort">Port number to connect to on the destination host.</param>
        ''' <remarks>
        ''' This method instructs the proxy server
        ''' to make a pass through connection to the specified destination host on the specified
        ''' port.  
        ''' </remarks>
        Public Sub CreateConnectionAsync(ByVal destinationHost As String, ByVal destinationPort As Integer) Implements SFDL.Proxy.Proxy.IProxyClient.CreateConnectionAsync
            If _asyncWorker IsNot Nothing AndAlso _asyncWorker.IsBusy Then
                Throw New InvalidOperationException("The Socks4 object is already busy executing another asynchronous operation.  You can only execute one asychronous method at a time.")
            End If

            CreateAsyncWorker()
            _asyncWorker.WorkerSupportsCancellation = True
            AddHandler _asyncWorker.DoWork, New DoWorkEventHandler(AddressOf CreateConnectionAsync_DoWork)
            AddHandler _asyncWorker.RunWorkerCompleted, New RunWorkerCompletedEventHandler(AddressOf CreateConnectionAsync_RunWorkerCompleted)
            Dim args As [Object]() = New [Object](1) {}
            args(0) = destinationHost
            args(1) = destinationPort
            _asyncWorker.RunWorkerAsync(args)
        End Sub

        Private Sub CreateConnectionAsync_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
            Try
                Dim args As [Object]() = DirectCast(e.Argument, [Object]())
                e.Result = CreateConnection(DirectCast(args(0), String), CInt(args(1)))
            Catch ex As Exception
                _asyncException = ex
            End Try
        End Sub

        Private Sub CreateConnectionAsync_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
            RaiseEvent CreateConnectionAsyncCompleted(Me, New CreateConnectionAsyncCompletedEventArgs(_asyncException, _asyncCancelled, DirectCast(e.Result, TcpClient)))
        End Sub



#End Region
    End Class
End Namespace